{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit Equaliser;

interface

uses Classes, SysUtils, XMLHelper;

const
  EQU_FILENAME = 'EqualiserSettings.xml';

  EQU_NUM_BANDS = 10; // Number of equaliser bands to use
  EQU_MAX_VALUE = 200; // Maximum value for band metadata settings
  EQU_OFFSET = -100; // Offset to give balanced +ve/-ve values from metadata
  DEF_EQU_VALUE = 0; // Default (centre) position for each band

  BASS_MAX_VALUE = 100; // Maximum value for true bass metadata settings
  BASS_MULTIPLIER = 20; // Scaling factor for true bass (range 0..2000)
  BASS_MIN_FREQ = 20; // Minimum frequency for bass boost metadata settings
  BASS_MAX_FREQ = 500; // Maximum frequency for bass boost metadata settings
  DEF_BASS_VALUE = 10; // Default bass boost amount
  DEF_BASS_FREQ = 200; // Default bass boost cutoff frequency (200hz)

  TREBLE_MAX_VALUE = 100; // Maximum value for treble boost metadata settings
  TREBLE_MULTIPLIER = 20; // Scaling factor for treble boost (range 0..2000)
  TREBLE_MIN_FREQ = 100; // Minimum frequency for treble boost metadata settings
  TREBLE_MAX_FREQ = 200; // Maximum frequency for treble boost metadata settings
  TREBLE_FREQ_MULT = 100; // Scaling factor for treble frequency (range 10-20Khz)
  DEF_TREBLE_VALUE = 10; // Default treble boost amount
  DEF_TREBLE_FREQ = 16000; // Default treble boost cutoff frequency (16Khz)

  SOUND3D_MAX_VALUE = 100; // Maximum value for 3D sound metadata settings
  SOUND3D_MULTIPLIER = 100; // Scaling factor for 3D sound (range 0..10000)
  DEF_SOUND3D_VALUE = 10; // Default 3D sound amount

type
  TAHMEqualiserBands = 1..EQU_NUM_BANDS;
  TAHMEqualiserValues = array[TAHMEqualiserBands] of Integer;

  TAHMEqualiserSettings = class
  private
    FName: String;
    FValues: TAHMEqualiserValues;
  protected
    function GetValue(Index: Integer): Integer;
    procedure SetValue(Index: Integer; Value: Integer);
  public
    constructor Create;
    property Name: String read FName write FName;
    property Values[Index: Integer]: Integer read GetValue write SetValue; default;
    procedure Assign(Source: TAHMEqualiserSettings);
    procedure ApplyDefaults;
  end;

  TAHMEqualiserList = class
  private
    FSettings: TList;
  protected
    function GetCount: Integer;
    function GetSettings(Index: Integer): TAHMEqualiserSettings;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Settings[Index: Integer]: TAHMEqualiserSettings read GetSettings; default;
    function Add(Name: String): TAHMEqualiserSettings;
    function FindSettings(Name: String): TAHMEqualiserSettings;
    procedure Clear;
  end;

  TAHMEqualiserManager = class
  private
    FHelper: TAHMXMLHelper;
    FEqs: TAHMEqualiserList;
    FCurrent: TAHMEqualiserSettings;
    FTrueBass: Boolean;
    FBassBoost: Integer;
    FBassFreq: Integer;
    FTreble: Boolean;
    FTrebleBoost: Integer;
    FTrebleFreq: Integer;
    FSound3D: Boolean;
    FSound3DBoost: Integer;
  protected
    function ParseXML(const Filepath: String): Boolean;
    procedure LogParsingError(const sErrorCode: String);
  public
    constructor Create;
    destructor Destroy; override;
    property CurrentEqualiser: TAHMEqualiserSettings read FCurrent;
    property TrueBass: Boolean read FTrueBass write FTrueBass;
    property BassBoost: Integer read FBassBoost write FBassBoost;
    property BassFrequency: Integer read FBassFreq write FBassFreq;
    property TrebleEnhance: Boolean read FTreble write FTreble;
    property TrebleBoost: Integer read FTrebleBoost write FTrebleBoost;
    property TrebleFrequency: Integer read FTrebleFreq write FTrebleFreq;
    property Sound3D: Boolean read FSound3D write FSound3D;
    property Sound3DBoost: Integer read FSound3DBoost write FSound3DBoost;
    procedure ResetDefaultSettings;
    procedure LoadEqualiserSettings;
    procedure SaveEqualiserSettings;
    procedure CopyPresetToCurrent(PresetName: String);
    procedure CopyCurrentToPreset(PresetName: String);
    procedure SaveAsXML(Lines: TStrings);
  end;

implementation

uses TagNames, XMLTags, ErrorCodes, ApplicationManager, Logging;

const
  EQU_PRESET_COUNT = 15;

  // Standard equaliser presets
  EQU_PRESET_NAMES: array[1..EQU_PRESET_COUNT] of String = ('Classical',
                    'Club', 'Dance', 'Full Bass', 'Full Bass & Treble',
                    'Full Treble', 'Large Hall', 'Live', 'Loudness',
                    'Party', 'Pop', 'Rock', 'Soft', 'Soft Rock', 'Techno');
  EQU_PRESETS: array[1..EQU_PRESET_COUNT] of TAHMEqualiserValues = (
               (0, 0, 0, 0, 0, -10, 40, 40, 40, 50),             // Classical
               (0, 0, -10, -20, -20, -20, -10, 0, 0, 0),         // Club
               (-50, -40, -20, 5, 0, 20, 30, 20, 0, 0),          // Dance
               (-50, -50, -50, -30, -10, 10, 30, 50, 50, 50),    // Full Bass
               (-30, -20, 0, 30, 20, -10, -40, -50, -50, -50),   // Bass&Treble
               (50, 50, 50, 20, -10, -50, -80, -80, -80, -90),   // Full Treble
               (-50, -50, -30, -20, 0, 20, 30, 30, 0, 0),        // Large Hall
               (30, 0, -10, -15, -20, -20, -10, -5, -5, 0),      // Live
               (-90, -70, -50, -20, 0, -10, -20, -50, -70, -90), // Loudness
               (-30, -30, 0, 0, 0, 0, 0, 0, -30, -30),           // Party
               (10, -15, -25, -25, -15, 10, 20, 20, 10, 10),     // Pop
               (-40, -30, 30, 40, 20, -20, -40, -40, -40, -40),  // Rock
               (-15, -5, 5, 15, 5, -15, -30, -40, -45, -50),     // Soft
               (-15, -15, -10, 5, 15, 25, 15, 5, -10, -30),      // Soft Rock
               (-30, -30, 0, 30, 20, 0, -20, -30, -30, -20));    // Techno

// TAHMEqualiserSettings

constructor TAHMEqualiserSettings.Create;
begin
  inherited;

  // Set default values for graphic equaliser profile
  ApplyDefaults;
end;

function TAHMEqualiserSettings.GetValue(Index: Integer): Integer;
begin
  Result := FValues[Index];
end;

procedure TAHMEqualiserSettings.SetValue(Index: Integer; Value: Integer);
begin
  FValues[Index] := Value;
end;

procedure TAHMEqualiserSettings.Assign(Source: TAHMEqualiserSettings);
var
  i: Integer;
begin
  // Copy values but don't change name
  if Source is TAHMEqualiserSettings then
    for i := Low(FValues) to High(FValues) do
      FValues[i] := Source.Values[i];
end;

procedure TAHMEqualiserSettings.ApplyDefaults;
var
  i: Integer;
begin
  // Set default values for graphic equaliser profile
  for i := Low(FValues) to High(FValues) do
    FValues[i] := DEF_EQU_VALUE;
end;

// TAHMEqualiserList

constructor TAHMEqualiserList.Create;
begin
  inherited;

  FSettings := TList.Create;
end;

destructor TAHMEqualiserList.Destroy;
begin
  Clear;
  FSettings.Free;

  inherited;
end;

function TAHMEqualiserList.GetCount: Integer;
begin
  Result := FSettings.Count;
end;

function TAHMEqualiserList.GetSettings(Index: Integer): TAHMEqualiserSettings;
begin
  if (Index >= 0) and (Index < FSettings.Count) then
    Result := TAHMEqualiserSettings(FSettings.Items[Index])
  else
    Result := nil;
end;

function TAHMEqualiserList.Add(Name: String): TAHMEqualiserSettings;
begin
  // Check for an existing equaliser with this name
  Result := FindSettings(Name);

  if Result = nil then
  begin
    Result := TAHMEqualiserSettings.Create;
    Result.Name := Name;
    FSettings.Add(Result);
  end;
end;

function TAHMEqualiserList.FindSettings(Name: String): TAHMEqualiserSettings;
var
  i: Integer;
begin
  for i := 0 to Pred(FSettings.Count) do
    if TAHMEqualiserSettings(FSettings.Items[i]).Name = Name then
    begin
      Result := TAHMEqualiserSettings(FSettings.Items[i]);
      Exit;
    end;

  // Set default result - not found
  Result := nil;
end;

procedure TAHMEqualiserList.Clear;
var
  i: Integer;
begin
  for i := 0 to Pred(FSettings.Count) do
    TAHMEqualiserSettings(FSettings.Items[i]).Free;

  FSettings.Clear;
end;

// TAHMEqualiserManager

constructor TAHMEqualiserManager.Create;
begin
  inherited Create;

  FEqs := TAHMEqualiserList.Create;
  FCurrent := TAHMEqualiserSettings.Create;

  // Initialise default equaliser values
  ResetDefaultSettings;
end;

destructor TAHMEqualiserManager.Destroy;
begin
  FEqs.Free;
  FCurrent.Free;

  inherited;
end;

procedure TAHMEqualiserManager.ResetDefaultSettings;
var
  i, j: Integer;
begin
  FCurrent.ApplyDefaults;

  // Remove any existing presets and populate defaults
  FEqs.Clear;
  for i := 1 to EQU_PRESET_COUNT do
    with FEqs.Add(EQU_PRESET_NAMES[i]) do
      for j := Low(TAHMEqualiserBands) to High(TAHMEqualiserBands) do
        Values[j] := EQU_PRESETS[i, j];

  // Apply default bass boost settings
  FTrueBass := False;
  FBassBoost := DEF_BASS_VALUE;
  FBassFreq := DEF_BASS_FREQ;

  // Apply default treble enhancement settings
  FTreble := False;
  FTrebleBoost := DEF_TREBLE_VALUE;
  FTrebleFreq := DEF_TREBLE_FREQ;

  // Apply default sound 3D settings
  FSound3D := False;
  FSound3DBoost := DEF_SOUND3D_VALUE;
end;

procedure TAHMEqualiserManager.LoadEqualiserSettings;
var
  Settings: TStringList;
begin
  // First set default equaliser values
  ResetDefaultSettings;

  // Parse XML to populate custom state values
  ParseXML(ApplicationManagerInstance.ConfigPath);

  // Log our state settings for support purposes
  Settings := TStringList.Create;
  try
    Settings.Add('mediate equaliser settings are as follows:');
    SaveAsXML(Settings);
    LogInfo(Settings);
  finally
    Settings.Free;
  end;
end;

procedure TAHMEqualiserManager.SaveEqualiserSettings;
var
  Settings: TStringList;
begin
  Settings := TStringList.Create;
  try
    Settings.Add(XML_PREAMBLE);
    Settings.Add(BeginTag(TAG_EQU_SETTINGS));
    SaveAsXML(Settings);
    Settings.Add(EndTag(TAG_EQU_SETTINGS));

    Settings.SaveToFile(ApplicationManagerInstance.ConfigPath + EQU_FILENAME);
  finally
    Settings.Free;
  end;
end;

procedure TAHMEqualiserManager.CopyPresetToCurrent(PresetName: String);
begin
//TODO CopyPresetToCurrent not implemented
end;

procedure TAHMEqualiserManager.CopyCurrentToPreset(PresetName: String);
begin
//TODO CopyCurrentToPreset not implemented
end;

function TAHMEqualiserManager.ParseXML(const Filepath: String): Boolean;
var
  sName: String;
  RootNode, NextNode, PresetNode, BandNode, EffectNode: TAHMXMLNode;
begin
  // Default result - problem loading equaliser file
  Result := False;

  // Don't bother trying to parse XML file if its missing
  if not FileExists(FilePath + EQU_FILENAME) then Exit;

  FHelper := TAHMXMLHelper.Create(Filepath + EQU_FILENAME);
  try
    // Ignore preamble - we're looking for an EqualiserSettings node
    if not FHelper.FindRootElement(RootNode, TAG_EQU_SETTINGS) then
      LogParsingError(XML_PRS_EQU_MISSING_START_TAG);

    // What nodes do we have here?
    NextNode := nil;
    while FHelper.NextElement(RootNode, NextNode) do
    begin
      if FHelper.CurrentElement = TAG_EQU_CURRENT then
      begin
        BandNode := nil;
        while FHelper.FindElement(NextNode, BandNode, TAG_EQU_BAND) do
          FCurrent[FHelper.GetIntAttribute(ATTR_NUMBER)] := FHelper.GetIntAttribute(ATTR_VALUE);
      end
      else if FHelper.CurrentElement = TAG_EQU_PRESETS then
      begin
        PresetNode := nil;
        while FHelper.FindElement(NextNode, PresetNode, TAG_EQU_PRESET) do
        begin
          sName := FHelper.GetAttribute(ATTR_NAME);
          if sName <> '' then with FEqs.Add(sName) do
          begin
            BandNode := nil;
            while FHelper.FindElement(PresetNode, BandNode, TAG_EQU_BAND) do
              Values[FHelper.GetIntAttribute(ATTR_NUMBER)] := FHelper.GetIntAttribute(ATTR_VALUE);
          end;
        end;
      end
      else if FHelper.CurrentElement = TAG_BASS_SETTINGS then
      begin
        EffectNode := nil;
        while FHelper.NextElement(NextNode, EffectNode) do
          if FHelper.CurrentElement = TAG_BASS_ENABLED then
            FTrueBass := Str2Bool(FHelper.GetValue)
          else if FHelper.CurrentElement = TAG_BASS_BOOST then
            FBassBoost := StrToIntDef(FHelper.GetValue, DEF_BASS_VALUE)
          else if FHelper.CurrentElement = TAG_BASS_FREQ then
            FBassFreq := StrToIntDef(FHelper.GetValue, DEF_BASS_FREQ);
      end
      else if FHelper.CurrentElement = TAG_TREBLE_SETTINGS then
      begin
        EffectNode := nil;
        while FHelper.NextElement(NextNode, EffectNode) do
          if FHelper.CurrentElement = TAG_TREBLE_ENABLED then
            FTreble := Str2Bool(FHelper.GetValue)
          else if FHelper.CurrentElement = TAG_TREBLE_BOOST then
            FTrebleBoost := StrToIntDef(FHelper.GetValue, DEF_TREBLE_VALUE)
          else if FHelper.CurrentElement = TAG_TREBLE_FREQ then
            FTrebleFreq := StrToIntDef(FHelper.GetValue, DEF_TREBLE_FREQ);
      end
      else if FHelper.CurrentElement = TAG_SOUND3D_SETTINGS then
      begin
        EffectNode := nil;
        while FHelper.NextElement(NextNode, EffectNode) do
          if FHelper.CurrentElement = TAG_SOUND3D_ENABLED then
            FSound3D := Str2Bool(FHelper.GetValue)
          else if FHelper.CurrentElement = TAG_SOUND3D_BOOST then
            FSound3DBoost := StrToIntDef(FHelper.GetValue, DEF_SOUND3D_VALUE);
      end
      else
        LogParsingError(XML_PRS_EQU_FOUND_UNKNOWN_CONTENT_TAG);
    end;

    // All is well, parsed equaliser file successfully
    Result := True;
  finally
    FHelper.Free;
  end;
end;

procedure TAHMEqualiserManager.SaveAsXML(Lines: TStrings);
var
  i, j: Integer;
  Settings: TAHMEqualiserSettings;
begin
  // Save bass boost settings
  Lines.Add(TABS[1] + BeginTag(TAG_BASS_SETTINGS));
  Lines.Add(TABS[2] + BeginTag(TAG_BASS_ENABLED) + Bool2Str(FTrueBass) + EndTag(TAG_BASS_ENABLED));
  Lines.Add(TABS[2] + BeginTag(TAG_BASS_BOOST) + IntToStr(FBassBoost) + EndTag(TAG_BASS_BOOST));
  Lines.Add(TABS[2] + BeginTag(TAG_BASS_FREQ) + IntToStr(FBassFreq) + EndTag(TAG_BASS_FREQ));
  Lines.Add(TABS[1] + EndTag(TAG_BASS_SETTINGS));

  // Save treble boost settings
  Lines.Add(TABS[1] + BeginTag(TAG_TREBLE_SETTINGS));
  Lines.Add(TABS[2] + BeginTag(TAG_TREBLE_ENABLED) + Bool2Str(FTreble) + EndTag(TAG_TREBLE_ENABLED));
  Lines.Add(TABS[2] + BeginTag(TAG_TREBLE_BOOST) + IntToStr(FTrebleBoost) + EndTag(TAG_TREBLE_BOOST));
  Lines.Add(TABS[2] + BeginTag(TAG_TREBLE_FREQ) + IntToStr(FTrebleFreq) + EndTag(TAG_TREBLE_FREQ));
  Lines.Add(TABS[1] + EndTag(TAG_TREBLE_SETTINGS));

  // Save sound 3D settings
  Lines.Add(TABS[1] + BeginTag(TAG_SOUND3D_SETTINGS));
  Lines.Add(TABS[2] + BeginTag(TAG_SOUND3D_ENABLED) + Bool2Str(FSound3D) + EndTag(TAG_SOUND3D_ENABLED));
  Lines.Add(TABS[2] + BeginTag(TAG_SOUND3D_BOOST) + IntToStr(FSound3DBoost) + EndTag(TAG_SOUND3D_BOOST));
  Lines.Add(TABS[1] + EndTag(TAG_SOUND3D_SETTINGS));

  // Save current equaliser settings
  Lines.Add(TABS[1] + BeginTag(TAG_EQU_CURRENT));
  for j := Low(TAHMEqualiserBands) to High(TAHMEqualiserBands) do
    Lines.Add(TABS[2] + TGB+TAG_EQU_BAND+SPC+ATTR_NUMBER+ATB + IntToStr(j) +
              ATN+ATTR_VALUE+ATB + IntToStr(FCurrent.Values[j]) + ATE);

  Lines.Add(TABS[1] + EndTag(TAG_EQU_CURRENT));
  Lines.Add(TABS[1] + BeginTag(TAG_EQU_PRESETS));

  // Loop through each of our equaliser presets
  for i := 0 to Pred(FEqs.Count) do
  begin
    Settings := FEqs.Settings[i];
    Lines.Add(TABS[2] + TGB+TAG_EQU_PRESET+SPC+ATTR_NAME+ATB + EncodeXML(Settings.Name) + ATC);

    // Loop through each of the bands for this profile
    for j := Low(TAHMEqualiserBands) to High(TAHMEqualiserBands) do
      Lines.Add(TABS[3] + TGB+TAG_EQU_BAND+SPC+ATTR_NUMBER+ATB + IntToStr(j) +
                ATN+ATTR_VALUE+ATB + IntToStr(Settings.Values[j]) + ATE);
    Lines.Add(TABS[2] + EndTag(TAG_EQU_PRESET));
  end;
  Lines.Add(TABS[1] + EndTag(TAG_EQU_PRESETS));
end;

procedure TAHMEqualiserManager.LogParsingError(const sErrorCode: String);
begin
  // Log error silently but there's no need to throw an exception
  LogWarn('Encountered error whilst parsing Equaliser XML: ' + sErrorCode);
end;

end.
